aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src/app/(main)/goals/[id]
diff options
context:
space:
mode:
authorLibravatarLibravatar Biswa Kalyan Bhuyan <biswa@surgot.in> 2025-04-27 23:02:42 +0530
committerLibravatarLibravatar Biswa Kalyan Bhuyan <biswa@surgot.in> 2025-04-27 23:02:42 +0530
commit538d933baef56d7ee76f78617b553d63713efa24 (patch)
tree3fcbc4208849dfa0e5dc8fe5761e103a3591c283 /frontend/src/app/(main)/goals/[id]
parent3941d80ff120238b973451325b834ebd8377281e (diff)
downloadfinance-538d933baef56d7ee76f78617b553d63713efa24.tar.gz
finance-538d933baef56d7ee76f78617b553d63713efa24.tar.bz2
finance-538d933baef56d7ee76f78617b553d63713efa24.zip
finance: feat: added the goal page with some improvements of ui
Diffstat (limited to 'frontend/src/app/(main)/goals/[id]')
-rw-r--r--frontend/src/app/(main)/goals/[id]/page.tsx290
1 files changed, 290 insertions, 0 deletions
diff --git a/frontend/src/app/(main)/goals/[id]/page.tsx b/frontend/src/app/(main)/goals/[id]/page.tsx
new file mode 100644
index 0000000..3428ca4
--- /dev/null
+++ b/frontend/src/app/(main)/goals/[id]/page.tsx
@@ -0,0 +1,290 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Progress } from "@/components/ui/progress";
+import { Badge } from "@/components/ui/badge";
+import { Edit, ArrowLeft, Loader2, RefreshCw } from "lucide-react";
+import { useToast } from "@/components/ui/use-toast";
+import { formatCurrency } from "@/lib/utils";
+import { api } from "@/lib/api";
+import { GoalProgress } from "../components/goals-list";
+
+export default function GoalDetailPage({ params }: { params: { id: string } }) {
+ const id = params.id;
+ const goalId = parseInt(id);
+
+ const [goal, setGoal] = useState<GoalWithProgress | null>(null);
+ const [loading, setLoading] = useState(true);
+ const [refreshing, setRefreshing] = useState(false);
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const fetchGoalDetails = useCallback(async () => {
+ try {
+ console.log(`Fetching goal details for ID: ${goalId}`);
+ setLoading(true);
+
+ // Add cache-busting parameter
+ const response = await api.get<GoalProgress>(`/goals/${goalId}/progress?cache=${new Date().getTime()}`);
+ console.log("Goal details received:", response.data);
+
+ // Validate and normalize data
+ const data = response.data;
+ if (data && data.goal) {
+ const sanitizedData = {
+ ...data,
+ goal: {
+ ...data.goal,
+ targetAmount: Number(data.goal.targetAmount) || 0,
+ currentAmount: Number(data.goal.currentAmount) || 0,
+ createdAt: data.goal.createdAt || new Date().toISOString(),
+ },
+ percentComplete: Number(data.percentComplete) || 0,
+ amountRemaining: Number(data.amountRemaining) || 0,
+ daysRemaining: Number(data.daysRemaining) || 0,
+ requiredPerDay: Number(data.requiredPerDay) || 0,
+ requiredPerMonth: Number(data.requiredPerMonth) || 0,
+ };
+ console.log("Processed goal data:", sanitizedData);
+ setGoal(sanitizedData);
+ } else {
+ console.error("Invalid goal data format:", data);
+ throw new Error("Invalid goal data received");
+ }
+ } catch (error) {
+ console.error("Error fetching goal details:", error);
+ toast({
+ title: "Error",
+ description: "Failed to fetch goal details. Please try again.",
+ variant: "destructive",
+ });
+ router.push("/goals");
+ } finally {
+ setLoading(false);
+ }
+ }, [goalId, toast, router]);
+
+ // Fetch goal details when component mounts
+ useEffect(() => {
+ if (!id) {
+ toast({
+ title: "Error",
+ description: "Goal ID is missing. Please try again.",
+ variant: "destructive",
+ });
+ router.push("/goals");
+ return;
+ }
+
+ fetchGoalDetails();
+ }, [id, fetchGoalDetails, router, toast]);
+
+ const recalculateProgress = async () => {
+ if (isNaN(goalId)) {
+ toast({
+ title: "Error",
+ description: "Invalid goal ID",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ try {
+ setRefreshing(true);
+ await api.post(`/goals/${goalId}/recalculate`);
+ toast({
+ title: "Progress recalculated",
+ description: "Your goal progress has been recalculated based on transactions.",
+ });
+ fetchGoalDetails();
+ } catch (error) {
+ toast({
+ title: "Error",
+ description: "Failed to recalculate goal progress. Please try again.",
+ variant: "destructive",
+ });
+ console.error("Error recalculating goal progress:", error);
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
+ if (loading) {
+ return (
+ <div className="container mx-auto py-8 flex justify-center items-center">
+ <Loader2 className="h-8 w-8 animate-spin" />
+ </div>
+ );
+ }
+
+ if (!goal) {
+ return (
+ <div className="container mx-auto py-8 text-center">
+ <p className="mb-4">Goal not found or access denied.</p>
+ <Link href="/goals">
+ <Button>Back to Goals</Button>
+ </Link>
+ </div>
+ );
+ }
+
+ const { goal: goalData, percentComplete, amountRemaining, daysRemaining, requiredPerDay, requiredPerMonth, onTrack } = goal;
+ const isCompleted = goalData.status === "Achieved";
+
+ return (
+ <div className="container mx-auto py-8">
+ <div className="mb-6">
+ <Link href="/goals">
+ <Button variant="ghost" size="sm">
+ <ArrowLeft className="mr-2 h-4 w-4" />
+ Back to Goals
+ </Button>
+ </Link>
+ </div>
+
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
+ <div>
+ <h1 className="text-2xl font-bold tracking-tight">{goalData.name}</h1>
+ <p className="text-muted-foreground">
+ {isCompleted
+ ? "Goal has been achieved 🎉"
+ : onTrack
+ ? "Progress is on track"
+ : "Progress is behind schedule"}
+ </p>
+ </div>
+ <div className="flex space-x-3 mt-4 md:mt-0">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={recalculateProgress}
+ disabled={refreshing}
+ >
+ {refreshing ? (
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ ) : (
+ <RefreshCw className="mr-2 h-4 w-4" />
+ )}
+ Recalculate
+ </Button>
+ <Link href={`/goals/edit/${goalData.id}`}>
+ <Button variant="outline" size="sm">
+ <Edit className="mr-2 h-4 w-4" />
+ Edit
+ </Button>
+ </Link>
+ </div>
+ </div>
+
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
+ <Card className="lg:col-span-2">
+ <CardHeader>
+ <div className="flex justify-between items-center">
+ <CardTitle>Goal Progress</CardTitle>
+ <Badge variant={isCompleted ? "default" : onTrack ? "outline" : "destructive"}>
+ {isCompleted ? "Achieved" : onTrack ? "On Track" : "Behind"}
+ </Badge>
+ </div>
+ </CardHeader>
+ <CardContent>
+ <div className="mb-6">
+ <div className="flex justify-between mb-2">
+ <span>Completion</span>
+ <span>{Math.round(percentComplete)}%</span>
+ </div>
+ <Progress value={percentComplete} className="h-3" />
+ </div>
+
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+ <div className="space-y-4">
+ <div>
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">Target Amount</h3>
+ <p className="text-2xl font-semibold">{formatCurrency(goalData.targetAmount)}</p>
+ </div>
+ <div>
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">Current Amount</h3>
+ <p className="text-2xl font-semibold">{formatCurrency(goalData.currentAmount)}</p>
+ </div>
+ <div>
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">Remaining</h3>
+ <p className="text-2xl font-semibold">{formatCurrency(amountRemaining)}</p>
+ </div>
+ </div>
+
+ <div className="space-y-4">
+ {goalData.targetDate && (
+ <div>
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">Target Date</h3>
+ <p className="text-xl font-semibold">{new Date(goalData.targetDate).toLocaleDateString()}</p>
+ </div>
+ )}
+ {daysRemaining > 0 && (
+ <>
+ <div>
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">Days Remaining</h3>
+ <p className="text-xl font-semibold">{daysRemaining} days</p>
+ </div>
+ <div>
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">Required Per Day</h3>
+ <p className="text-xl font-semibold">{formatCurrency(requiredPerDay)}</p>
+ </div>
+ <div>
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">Required Per Month</h3>
+ <p className="text-xl font-semibold">{formatCurrency(requiredPerMonth)}</p>
+ </div>
+ </>
+ )}
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ <Card>
+ <CardHeader>
+ <CardTitle>Goal Details</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ <div>
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">Goal Name</h3>
+ <p className="font-medium">{goalData.name}</p>
+ </div>
+ <div>
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">Purpose</h3>
+ <p>{goalData.name}</p>
+ </div>
+ <div>
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">Status</h3>
+ <p>{goalData.status}</p>
+ </div>
+ <div>
+ <h3 className="text-sm font-medium text-muted-foreground mb-1">Created</h3>
+ <p>{new Date(goalData.createdAt).toLocaleDateString()}</p>
+ </div>
+ {isCompleted ? (
+ <div className="pt-4">
+ <div className="p-4 bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300 rounded-md">
+ <p className="font-semibold">🎉 Goal achieved!</p>
+ <p className="text-sm mt-1">
+ Congratulations on achieving your financial goal.
+ </p>
+ </div>
+ </div>
+ ) : (
+ <div className="pt-4">
+ <Link href={`/transactions?goalId=${goalData.id}`}>
+ <Button variant="secondary" className="w-full">View Related Transactions</Button>
+ </Link>
+ </div>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+ );
+} \ No newline at end of file